//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace Microsoft.Research.eResearch.WFServiceLibrary.SchedulerLogic
{
    using System;
    using System.Collections.Generic;
    using System.Runtime.InteropServices;
    using System.Security.Principal;
    using V1 = TaskScheduler.V1;
    using V2 = TaskScheduler;
    using Microsoft.Research.eResearch.Common.Logging;
    using Microsoft.Research.eResearch.Common.Eventing;
    using System.Globalization;
    using System.Linq;

    /// <summary>
    /// Scheduler trigger wrapper.
    /// </summary>
    public abstract class Trigger : IDisposable, ILoggable
    {
        /// <summary>
        /// Defines how often the task is run.
        /// </summary>
        private RepetitionPattern repititionPattern = null;

        /// <summary>
        /// Keep unbounded objects here.
        /// </summary>
        protected Dictionary<string, object> unboundValues;

        /// <summary>
        /// Schedule creation date format.
        /// </summary>
        internal const string TaskBoundaryDateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'FFF";

        /// <summary>
        /// Type of triggers that can be used by tasks.
        /// </summary>
        private TaskTriggerType triggerType;

        /// <summary>
        /// Gets or sets the task scheduler trigger.
        /// </summary>
        /// <value>The task scheduler trigger.</value>
        public V2.ITrigger V2TaskSchedulerTrigger { get; set; }

        /// <summary>
        /// Gets or sets the name of the task.
        /// </summary>
        /// <value>The name of the task.</value>
        public string TaskName { get; set; }

        /// <summary>
        /// Gets or sets the task description.
        /// </summary>
        /// <value>The task description.</value>
        public string TaskDescription { get; set; }

        /// <summary>
        /// Gets or sets the executable path.
        /// </summary>
        /// <value>The executable path.</value>
        public string ExecutablePath { get; set; }

        /// <summary>
        /// Gets or sets the arguments.
        /// </summary>
        /// <value>The arguments.</value>
        public string Arguments { get; set; }

        /// <summary>
        /// Task trigger for older version.
        /// </summary>
        internal V1.ITaskTrigger v1Trigger;

        /// <summary>
        /// Older version task trigger data.
        /// </summary>
        internal V1.TaskTrigger v1TriggerData;

        /// <summary>
        /// Gets a <see cref="RepetitionPattern"/> instance that indicates how often the task is run and how long the repetition pattern is repeated after the task is started.
        /// </summary>
        public RepetitionPattern Repetition
        {
            get
            {
                if (this.repititionPattern == null)
                {
                    if (this.V2TaskSchedulerTrigger != null)
                    {
                        this.repititionPattern = new RepetitionPattern(this.V2TaskSchedulerTrigger.Repetition);
                    }
                    else
                    {
                        this.repititionPattern = new RepetitionPattern(this.v1Trigger, this);
                    }
                }

                return this.repititionPattern;
            }
        }

        /// <summary>
        /// Gets or sets the date and time when the trigger is activated.
        /// </summary>
        public DateTime StartBoundary
        {
            get
            {
                if (this.V2TaskSchedulerTrigger != null)
                {
                    return DateTime.Parse(this.V2TaskSchedulerTrigger.StartBoundary);
                }

                return v1TriggerData.BeginDate;
            }

            set
            {
                if (this.V2TaskSchedulerTrigger != null)
                {
                    this.V2TaskSchedulerTrigger.StartBoundary = value.ToString(TaskBoundaryDateFormat);
                }
                else
                {
                    v1TriggerData.BeginDate = value;
                    if (v1Trigger != null)
                        SetV1TriggerData();
                    else
                        unboundValues["StartBoundary"] = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the date and time when the trigger is deactivated. 
        /// The trigger cannot start the task after it is deactivated.
        /// </summary>
        public DateTime EndBoundary
        {
            get
            {
                DateTime endTime;
                if (this.V2TaskSchedulerTrigger != null)
                {
                    endTime = string.IsNullOrEmpty(this.V2TaskSchedulerTrigger.EndBoundary) ?
                        DateTime.MaxValue :
                        DateTime.Parse(this.V2TaskSchedulerTrigger.EndBoundary);
                }
                else
                {
                    endTime = v1TriggerData.EndDate;
                }

                return endTime;
            }

            set
            {
                if (this.V2TaskSchedulerTrigger != null)
                {
                    this.V2TaskSchedulerTrigger.EndBoundary = value == DateTime.MaxValue ?
                        null :
                        value.ToString(TaskBoundaryDateFormat);
                }
                else
                {
                    v1TriggerData.EndDate = value;
                    if (v1Trigger != null)
                        SetV1TriggerData();
                    else
                        unboundValues["EndBoundary"] = value;
                }
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Trigger"/> class.
        /// </summary>
        /// <param name="trigger">The trigger.</param>
        /// <param name="type">The type.</param>
        internal Trigger(V1.ITaskTrigger trigger, V1.TaskTriggerType type) :
            this(trigger, trigger.GetTrigger())
        {
            v1TriggerData.TriggerSize = (ushort)Marshal.SizeOf(typeof(V1.TaskTrigger));
            v1TriggerData.Type = type;
            v1TriggerData.Flags = 0;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Trigger"/> class.
        /// </summary>
        /// <param name="trigger">The trigger.</param>
        /// <param name="data">The data.</param>
        internal Trigger(V1.ITaskTrigger trigger, V1.TaskTrigger data)
        {
            v1Trigger = trigger;
            v1TriggerData = data;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Trigger"/> class.
        /// </summary>
        /// <param name="iTrigger">The i trigger.</param>
        internal Trigger(V2.ITrigger iTrigger)
        {
            this.V2TaskSchedulerTrigger = iTrigger;
            if (string.IsNullOrEmpty(this.V2TaskSchedulerTrigger.StartBoundary))
            {
                this.StartBoundary = DateTime.Now;
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Trigger"/> class.
        /// </summary>
        /// <param name="triggerType">Type of the trigger.</param>
        internal Trigger(TaskTriggerType triggerType)
        {
            this.triggerType = triggerType;
            this.unboundValues = new Dictionary<string, object>();

            v1TriggerData = new V1.TaskTrigger();
            v1TriggerData.TriggerSize = (ushort)Marshal.SizeOf(typeof(V1.TaskTrigger));
            try
            {
                v1TriggerData.Type = ConvertToV1TriggerType(this.triggerType);
            }
            catch { }

            this.StartBoundary = DateTime.Now;
        }

        /// <summary>
        /// Releases all resources used by this class.
        /// </summary>
        public virtual void Dispose()
        {
            if (this.V2TaskSchedulerTrigger != null)
            {
                Marshal.ReleaseComObject(this.V2TaskSchedulerTrigger);
            }
        }

        internal static V1.TaskTriggerType ConvertToV1TriggerType(TaskTriggerType type)
        {
            int v1tt = (int)type - 1;
            if (v1tt >= 7) v1tt--;
            return (V1.TaskTriggerType)v1tt;
        }

        /// <summary>In testing and may change. Do not use until officially introduced into library.</summary>
        internal void SetV1TriggerData()
        {
            if (v1Trigger != null)
                v1Trigger.SetTrigger(ref v1TriggerData);
            System.Diagnostics.Debug.WriteLine(v1TriggerData);
        }

        /// <summary>In testing and may change. Do not use until officially introduced into library.</summary>
        internal virtual void Bind(V1.ITask iTask)
        {
            if (v1Trigger == null)
            {
                ushort idx;
                v1Trigger = iTask.CreateTrigger(out idx);
            }
            SetV1TriggerData();
        }

        /// <summary>
        /// Binds the specified task def.
        /// </summary>
        /// <param name="iTaskDef">Task def.</param>
        internal virtual void Bind(V2.ITaskDefinition iTaskDef)
        {
            V2.ITriggerCollection iTriggers = iTaskDef.Triggers;
            this.V2TaskSchedulerTrigger = iTriggers.Create((V2._TASK_TRIGGER_TYPE2)(int)this.triggerType);
            Marshal.ReleaseComObject(iTriggers);
            foreach (string key in this.unboundValues.Keys)
            {
                try
                {
                    object timeObject = this.unboundValues[key];
                    if (timeObject is TimeSpan)
                    {
                        timeObject = Task.TimeSpanToString((TimeSpan)timeObject);
                    }

                    if (timeObject is DateTime)
                    {
                        timeObject = ((DateTime)timeObject).ToString(TaskBoundaryDateFormat);
                    }

                    this.V2TaskSchedulerTrigger.GetType().InvokeMember(
                        key,
                        System.Reflection.BindingFlags.SetProperty,
                        null,
                        this.V2TaskSchedulerTrigger,
                        new object[] { timeObject });
                }
                catch (System.Reflection.TargetInvocationException tie)
                {
                    throw tie.InnerException;
                }
            }

            this.unboundValues.Clear();
            this.unboundValues = null;
        }

        internal static Trigger CreateTrigger(V1.ITaskTrigger trigger)
        {
            return CreateTrigger(trigger, trigger.GetTrigger().Type);
        }

        internal static Trigger CreateTrigger(V1.ITaskTrigger trigger, V1.TaskTriggerType triggerType)
        {
            switch (triggerType)
            {
                case V1.TaskTriggerType.RunOnce:
                    return new TimeTrigger(trigger);
                case V1.TaskTriggerType.RunDaily:
                    return new DailyTrigger(trigger);
                case V1.TaskTriggerType.RunWeekly:
                    return new WeeklyTrigger(trigger);
                default:
                    break;
            }
            return null;
        }

        /// <summary>
        /// Creates the trigger.
        /// </summary>
        /// <param name="iTrigger">The i trigger.</param>
        /// <returns>Created trigger.</returns>
        internal static Trigger CreateTrigger(V2.ITrigger iTrigger)
        {
            switch (iTrigger.Type)
            {
                case V2._TASK_TRIGGER_TYPE2.TASK_TRIGGER_DAILY:
                    return new DailyTrigger((V2.IDailyTrigger)iTrigger);
                case V2._TASK_TRIGGER_TYPE2.TASK_TRIGGER_TIME:
                    return new TimeTrigger((V2.ITimeTrigger)iTrigger);
                case V2._TASK_TRIGGER_TYPE2.TASK_TRIGGER_WEEKLY:
                    return new WeeklyTrigger((V2.IWeeklyTrigger)iTrigger);
                default:
                    break;
            }

            return null;
        }

        /// <summary>
        /// Creates the task.
        /// </summary>
        /// <param name="isNewTask">if set to <c>true</c> [is new task].</param>
        /// <returns>Newly created task.</returns>
        public void CreateTask(bool isNewTask)
        {
            try
            {
                TaskService taskService = new TaskService();
                Version ver = taskService.HighestSupportedVersion;
                bool newVer = (ver == new Version(1, 2) || ver == new Version(1, 3));

                TaskFolder tf = taskService.RootFolder;

                Task task = null;
                TaskDefinition taskDefinition;

                TaskCreation taskCreationType = TaskCreation.Create;
                if (!isNewTask)
                {
                    if (newVer)
                    {
                        task = taskService.RootFolder.Tasks.FirstOrDefault(T => T.Name.Equals(this.TaskName, StringComparison.OrdinalIgnoreCase));
                        if (task != null)
                        {
                            task = taskService.RootFolder.Tasks[this.TaskName];
                            taskDefinition = task.Definition;
                            taskCreationType = TaskCreation.Update;
                        }
                        else
                        {
                            taskDefinition = taskService.NewTask();
                        }
                    }
                    else
                    {
                        task = taskService.RootFolder.Tasks.FirstOrDefault(T => T.Name.Equals(this.TaskName, StringComparison.OrdinalIgnoreCase));
                        if (task != null)
                        {
                            taskService.RootFolder.DeleteTask(this.TaskName);
                            taskCreationType = TaskCreation.Update;
                        }

                        taskDefinition = taskService.NewTask();
                    }
                }
                else
                {
                    taskCreationType = TaskCreation.Create;
                    taskDefinition = taskService.NewTask();
                }

                taskDefinition.RegistrationInfo.Description = this.TaskDescription;

                taskDefinition.Triggers.Clear();
                taskDefinition.Triggers.Add(this);

                taskDefinition.Actions.Clear();
                taskDefinition.Actions.Add(new ExecAction(this.ExecutablePath, this.Arguments, null));

                V2._TASK_LOGON_TYPE logonType = V2._TASK_LOGON_TYPE.TASK_LOGON_INTERACTIVE_TOKEN;
                if (newVer)
                {
                    taskDefinition.Principal.Id = WindowsIdentity.GetCurrent().Name;
                }

                task = taskService.RootFolder.RegisterTaskDefinition(
                        this.TaskName,
                        taskDefinition,
                        taskCreationType,
                        null,
                        null,
                        logonType, "");


                taskService.Dispose();
            }
            catch (Exception ex)
            {
                this.Log("Error", "Error in task creation : {0}", ex.Message);
            }
        }

        #region ILoggable Members

        /// <summary>
        /// Fired when a log message is received.
        /// </summary>
        public event EventHandler<LogEventArgs> LogMessageReceived;

        /// <summary>
        /// Raises the LogMessageReceived event.
        /// </summary>
        /// <param name="level">The log level.</param>
        /// <param name="message">The log message.</param>
        protected virtual void OnLogMessageReceived(string level, string message)
        {
            this.LogMessageReceived.Fire(this, new LogEventArgs(level, message));
        }

        /// <summary>
        /// Writes the log message.
        /// </summary>
        /// <param name="level">The log level.</param>
        /// <param name="message">Message string to be logged.</param>
        private void Log(string level, string message)
        {
            this.OnLogMessageReceived(level, String.Format(CultureInfo.CurrentUICulture, "ServerScheduler: {0}", message));
        }

        /// <summary>
        /// Writes the log message.
        /// </summary>
        /// <param name="level">The log level.</param>
        /// <param name="format">Format of the message.</param>
        /// <param name="args">Arguments to be formatted.</param>
        private void Log(string level, string format, params object[] args)
        {
            this.Log(level, String.Format(CultureInfo.CurrentUICulture, format, args));
        }

        #endregion
    }

    /// <summary>
    /// Represents a trigger that starts a task at a specific date and time.
    /// </summary>
    public sealed class TimeTrigger : Trigger
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="TimeTrigger"/> class.
        /// </summary>
        /// <param name="iTrigger">The i trigger.</param>
        internal TimeTrigger(V1.ITaskTrigger iTrigger) :
            base(iTrigger, V1.TaskTriggerType.RunOnce) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="TimeTrigger"/> class.
        /// </summary>
        /// <param name="iTrigger">The i trigger.</param>
        internal TimeTrigger(V2.ITrigger iTrigger)
            : base(iTrigger)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="TimeTrigger"/> class.
        /// </summary>
        public TimeTrigger()
            : base(TaskTriggerType.Time)
        {
        }
    }

    /// <summary>
    /// Represents a trigger that starts a task based on a daily schedule. For example, the task starts at a specific time every day, every other day, every third day, and so on.
    /// </summary>
    public sealed class DailyTrigger : Trigger
    {
        internal DailyTrigger(V1.ITaskTrigger iTrigger) :
            base(iTrigger, V1.TaskTriggerType.RunDaily) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="DailyTrigger"/> class.
        /// </summary>
        /// <param name="iTrigger">The i trigger.</param>
        internal DailyTrigger(V2.ITrigger iTrigger)
            : base(iTrigger)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="DailyTrigger"/> class.
        /// </summary>
        /// <param name="daysInterval">The days interval.</param>
        public DailyTrigger(short daysInterval)
            : base(TaskTriggerType.Daily)
        {
            this.DaysInterval = daysInterval;
        }

        /// <summary>
        /// Gets or sets interval between the days in the schedule.
        /// </summary>
        /// <value>The days interval.</value>
        public short DaysInterval
        {
            get
            {
                short daysInterval;
                if (V2TaskSchedulerTrigger != null)
                {
                    daysInterval = ((V2.IDailyTrigger)V2TaskSchedulerTrigger).DaysInterval;
                }
                else
                {
                    daysInterval = (short)v1TriggerData.Data.daily.DaysInterval;
                }

                return daysInterval;
            }

            set
            {
                if (V2TaskSchedulerTrigger != null)
                {
                    ((V2.IDailyTrigger)V2TaskSchedulerTrigger).DaysInterval = value;
                }
                else
                {
                    v1TriggerData.Data.daily.DaysInterval = (ushort)value;
                    if (v1Trigger != null)
                        SetV1TriggerData();
                    else
                        unboundValues["DaysInterval"] = value;
                }
            }
        }
    }

    /// <summary>
    /// Represents a trigger that starts a task based on a weekly schedule. For example, the task starts at 8:00 A.M. on a specific day of the week every week or every other week.
    /// </summary>
    public sealed class WeeklyTrigger : Trigger
    {
        internal WeeklyTrigger(V1.ITaskTrigger iTrigger) :
            base(iTrigger, V1.TaskTriggerType.RunWeekly) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="WeeklyTrigger"/> class.
        /// </summary>
        /// <param name="iTrigger">The i trigger.</param>
        internal WeeklyTrigger(V2.ITrigger iTrigger) :
            base(iTrigger)
        {
        }

        /// <summary>
        /// Initializes an unbound instance of <see cref="WeeklyTrigger"/> class.
        /// </summary>
        /// <param name="days">Sum of week days.</param>
        /// <param name="weekInterval">The week interval.</param>
        public WeeklyTrigger(short days, short weekInterval)
            : base(TaskTriggerType.Weekly)
        {
            this.DaysOfWeek = (DaysOfTheWeek)days;
            this.WeeksInterval = weekInterval;
        }

        /// <summary>
        /// Gets or sets the days of the week on which the task runs.
        /// </summary>
        public DaysOfTheWeek DaysOfWeek
        {
            get
            {
                DaysOfTheWeek daysOfTheWeek = DaysOfTheWeek.None;
                if (this.V2TaskSchedulerTrigger != null)
                {
                    daysOfTheWeek = this.DaysOfWeek;
                }
                else
                {
                    daysOfTheWeek = (DaysOfTheWeek)v1TriggerData.Data.weekly.DaysOfTheWeek;
                }

                return daysOfTheWeek;
            }

            set
            {
                if (this.V2TaskSchedulerTrigger != null)
                {
                    ((V2.IWeeklyTrigger)V2TaskSchedulerTrigger).DaysOfWeek = (short)value;
                }
                else
                {
                    v1TriggerData.Data.weekly.DaysOfTheWeek = (ushort)value;
                    if (v1Trigger != null)
                        SetV1TriggerData();
                    else
                        unboundValues["DaysOfWeek"] = (short)value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the interval between the weeks in the schedule.
        /// </summary>
        public short WeeksInterval
        {
            get
            {
                short weekIntervals;
                if (V2TaskSchedulerTrigger != null)
                {
                    weekIntervals = ((V2.IWeeklyTrigger)V2TaskSchedulerTrigger).WeeksInterval;
                }
                else
                {
                    weekIntervals = (short)v1TriggerData.Data.weekly.WeeksInterval;
                }

                return weekIntervals;
            }

            set
            {
                if (V2TaskSchedulerTrigger != null)
                {
                    ((V2.IWeeklyTrigger)V2TaskSchedulerTrigger).WeeksInterval = value;
                }
                else
                {
                    v1TriggerData.Data.weekly.WeeksInterval = (ushort)value;
                    if (v1Trigger != null)
                        SetV1TriggerData();
                    else
                        unboundValues["WeeksInterval"] = value;
                }
            }
        }
    }

    /// <summary>
    /// Defines how often the task is run and how long the repetition pattern is 
    /// repeated after the task is started.
    /// </summary>
    public sealed class RepetitionPattern : IDisposable
    {
        private V1.ITaskTrigger v1Trigger = null;
        private Trigger pTrigger;

        /// <summary>
        /// Task repetition pattern
        /// </summary>
        private V2.IRepetitionPattern taskRepetitionPattern = null;

        /// <summary>
        /// Initializes a new instance of the <see cref="RepetitionPattern"/> class.
        /// </summary>
        /// <param name="trigger">The trigger.</param>
        /// <param name="parent">The parent.</param>
        internal RepetitionPattern(V1.ITaskTrigger trigger, Trigger parent)
        {
            this.v1Trigger = trigger;
            this.pTrigger = parent;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RepetitionPattern"/> class.
        /// </summary>
        /// <param name="pattern">The pattern.</param>
        internal RepetitionPattern(V2.IRepetitionPattern pattern)
        {
            this.taskRepetitionPattern = pattern;
        }

        /// <summary>
        /// Releases all resources used by this class.
        /// </summary>
        public void Dispose()
        {
            if (this.taskRepetitionPattern != null)
            {
                Marshal.ReleaseComObject(this.taskRepetitionPattern);
            }

            this.v1Trigger = null;
        }
    }

    /// <summary>
    /// Defines the type of triggers that can be used by tasks.
    /// </summary>
    public enum TaskTriggerType
    {
        /// <summary>Triggers the task when the computer boots.</summary>
        Boot = 8,

        /// <summary>Triggers the task on a daily schedule.</summary>
        Daily = 2,

        /// <summary>Triggers the task when a specific event occurs. Version 1.2 only.</summary>
        Event = 0,

        /// <summary>Triggers the task when the computer goes into an idle state.</summary>
        Idle = 6,

        /// <summary>Triggers the task when a specific user logs on.</summary>
        Logon = 9,

        /// <summary>Triggers the task on a monthly schedule.</summary>
        Monthly = 4,

        /// <summary>Triggers the task on a monthly day-of-week schedule.</summary>
        MonthlyDOW = 5,

        /// <summary>Triggers the task when the task is registered. Version 1.2 only.</summary>
        Registration = 7,

        /// <summary>Triggers the task when a specific user session state changes. Version 1.2 only.</summary>
        SessionStateChange = 11,

        /// <summary>Triggers the task at a specific time of day.</summary>
        Time = 1,

        /// <summary>Triggers the task on a weekly schedule.</summary>
        Weekly = 3
    }

    /// <summary>
    /// Values for days of the week (Monday, Tuesday, etc.)
    /// </summary>
    [Flags]
    public enum DaysOfTheWeek : short
    {
        /// <summary>None</summary>
        None = 0x00,

        /// <summary>Sunday</summary>
        Sunday = 0x1,

        /// <summary>Monday</summary>
        Monday = 0x2,

        /// <summary>Tuesday</summary>
        Tuesday = 0x4,

        /// <summary>Wednesday</summary>
        Wednesday = 0x8,

        /// <summary>Thursday</summary>
        Thursday = 0x10,

        /// <summary>Friday</summary>
        Friday = 0x20,

        /// <summary>Saturday</summary>
        Saturday = 0x40,

        /// <summary>All days</summary>
        AllDays = 0x7F
    }
}
